package com.minelx.serverandclient.stream.data;

import com.minelx.serverandclient.common.Holder;
import com.minelx.serverandclient.common.EnsuredString;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import static java.lang.System.arraycopy;
import static java.util.Arrays.copyOfRange;

public class StreamCache {

	public static final int TOTAL_SIZE = 256;

	public static final int FLAG_SIZE = 1;

	private static final int FLAG_DELTA = 128;

	public static final int DATA_SIZE = TOTAL_SIZE - FLAG_SIZE; // 255

	private final byte[] cacheBytes;

	/**
	 * initialize an empty data cache.
	 */
	private StreamCache() {
		this(new byte[TOTAL_SIZE]);
	}

	/**
	 * initialize a cache with formatted data bytes.
	 *
	 * @param cacheBytes formatted data bytes
	 */
	private StreamCache(byte[] cacheBytes) {
		this.cacheBytes = cacheBytes;
	}

	/**
	 * get the cache-formatted bytes.
	 *
	 * @return resulting
	 */
	public byte[] getCacheBytes() {
		return cacheBytes;
	}

	/**
	 * is this an empty data cache.
	 *
	 * @return true if it's empty, false otherwise.
	 */
	public boolean emptyData() {
		return getDataLength() == 0;
	}

	/**
	 * get the length of data.
	 *
	 * @return resulting
	 */
	public int getDataLength() {
		return cacheBytes[0] + FLAG_DELTA;
	}

	/**
	 * render source bytes to data
	 *
	 * @param source source bytes
	 * @param start  where we start in source
	 * @param len    how many bytes we'll render
	 * @return self
	 */
	public StreamCache render(byte[] source, int start, int len) {
		setDataLength(len);
		arraycopy(source, start, cacheBytes, 1, len);
		return this;
	}

	/**
	 * set length of data.
	 *
	 * @param length length
	 */
	private void setDataLength(int length) {
		if (length > DATA_SIZE) {
			throw new IllegalArgumentException("invalid cache data length: " + length);
		}
		cacheBytes[0] = (byte) (length - FLAG_DELTA);
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof StreamCache cache) {
			return equalsTo(cache);
		}
		return false;
	}

	@Override
	public String toString() {
		return "StreamCache" + EnsuredString
				.of(Arrays.toString(cacheBytes))
				.cap("(", ")")
				.toString();
	}

	private boolean equalsTo(StreamCache another) {
		if (getDataLength() != another.getDataLength()) {
			return false;
		}
		return Arrays.equals(
				copyOfRange(getCacheBytes(), 1, getDataLength()),
				copyOfRange(another.getCacheBytes(), 1, getDataLength())
		);
	}

	/**
	 * initialize StreamCache with an formatted cache data.
	 *
	 * @param formattedCache already formatted cache data
	 * @return resulting
	 */
	public static StreamCache of(byte[] formattedCache) {
		return new StreamCache(formattedCache);
	}

	/**
	 * initialize StreamCache with formatting the specified raw data.
	 *
	 * @param dataBytes raw data
	 * @return resulting
	 */
	public static StreamCache ofRaw(byte[] dataBytes) {
		return Renderer.simple().render(dataBytes);
	}

	/**
	 * initialize empty StreamCache.
	 *
	 * @return resulting
	 */
	public static StreamCache empty() {
		return new StreamCache();
	}

	public static class Renderer {
		private final Holder holder;

		private Renderer(Holder holder) {
			this.holder = holder;
		}

		/**
		 * render StreamCache.<br>
		 *
		 * @param dataBytes raw data source
		 * @return resulting
		 *
		 * @see #render(byte[], int, int)
		 */
		public StreamCache render(byte[] dataBytes) {
			return render(dataBytes, 0, dataBytes.length);
		}

		/**
		 * render StreamCache.
		 *
		 * @param dataBytes raw data source
		 * @param start     start offset
		 * @param len       length that being rendered
		 * @return resulting
		 */
		public StreamCache render(byte[] dataBytes, int start, int len) {
			holder.get().render(dataBytes, start, len);
			return holder.fetch();
		}

		/**
		 * render StreamCache.
		 *
		 * @param inputStream source inputStream
		 * @return resulting
		 * @throws IOException if read() throws ...
		 */
		public StreamCache render(InputStream inputStream) throws IOException {
			if (inputStream.read(holder.get().cacheBytes) != StreamCache.TOTAL_SIZE) {
				throw new IllegalArgumentException("it's not a Stream for sending StreamCache data. \n\t\tinvalid target is: " + Arrays.toString(holder.get().cacheBytes));
			}
			return holder.get();
		}

		/**
		 * simple Builder for StreamCache.
		 *
		 * @return result
		 */
		public static Renderer simple() {
			return new Renderer(Holder.simple());
		}

		/**
		 * reuse the cache object in builder.
		 *
		 * @return reusing Builder
		 */
		public static Renderer singleton() {
			return new Renderer(Holder.singleton());
		}
	}
}
